#!/usr/bin/env python3
# ===----------------------------------------------------------------------===##
#
# This source file is part of the Swift open source project
#
# Copyright (c) 2025 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ===----------------------------------------------------------------------===##

import argparse
import dataclasses
import itertools
import logging
import os
import pathlib
import platform
import shlex
import sys
from datetime import datetime

import typing as t

from helpers import (
    change_directory,
    call,
    call_output,
)

logging.basicConfig(
    format=" | ".join(
        [
            # Prefix script name to the log in an attempt to avoid confusion when parsing logs
            f"{pathlib.Path(sys.argv[0]).name}",
            "%(asctime)s",
            "%(levelname)-8s",
            "%(module)s",
            "%(funcName)s",
            "Line:%(lineno)d",
            "%(message)s",
        ]
    ),
    level=logging.INFO,
)


REPO_ROOT_PATH = pathlib.Path(__file__).parent.parent.resolve()


class UnsupportedArchitecture(Exception):

    pass


def get_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )

    parser.add_argument(
        "-v",
        "--verbose",
        dest="is_verbose",
        action="store_true",
        help="When set, prints verbose information.",
    )
    parser.add_argument(
        "-c",
        "--configuration",
        type=str,
        dest="config",
        help="The configuration to use.",
    )
    parser.add_argument(
        "-t",
        "--triple",
        type=str,
        dest="triple",
    )
    parser.add_argument(
        "-b",
        "--build-system",
        type=str,
        dest="build_system",
    )
    parser.add_argument(
        "--skip-clean",
        action="store_false",
        dest="should_clean",
        help="When set, do not clean the build output"
    )
    parser.add_argument(
        "--skip-update",
        action="store_false",
        dest="should_update",
        help="When set, do not run a swift package update"
    )
    parser.add_argument(
        "--additional-build-args",
        type=str,
        dest="additional_build_args",
        default=""
    )
    parser.add_argument(
        "--additional-run-args",
        type=str,
        dest="additional_run_args",
        default=""
    )
    parser.add_argument(
        "--additional-test-args",
        type=str,
        dest="additional_test_args",
        default=""
    )
    parser.add_argument(
        "--skip-bootstrap",
        dest="skip_bootstrap",
        action="store_true"
    )
    parser.set_defaults(skip_bootstrap=False)
    args = parser.parse_args()
    return args


def log_environment() -> None:
    logging.info("Environment Variables")
    for key, value in sorted(os.environ.items()):
        logging.info("  --> %s=%r", key, value)


def get_swiftpm_bin_dir(
        *,
        global_args: t.List[str],
) -> pathlib.Path:
    logging.info("Retrieving Swift PM binary directory.")
    swiftpm_bin_dir = pathlib.Path(
        call_output(["swift", "build", *global_args, "--show-bin-path"])
    )
    logging.info("SwiftPM BIN DIR: %s", swiftpm_bin_dir)
    return swiftpm_bin_dir


def is_on_darwin() -> bool:
    return platform.system() == "Darwin"


def set_environment() -> None:
    os.environ["SWIFTCI_IS_SELF_HOSTED"] = "1"

    # Ensure SDKROOT is configure
    if is_on_darwin():
        sdk_root = call_output(shlex.split("xcrun --show-sdk-path --sdk macosx"))
        logging.debug("macos sdk root = %r", sdk_root)
        os.environ["SDKROOT"] = sdk_root
    log_environment()


def run_bootstrap(swiftpm_bin_dir: pathlib.Path) -> None:
    logging.info("Current working directory is %s", pathlib.Path.cwd())
    logging.info("Bootstrapping with the XCBuild codepath...")
    cross_compile_arch: str
    arch = platform.machine()
    if arch == "arm64":
        cross_compile_arch = "x86_64"
    elif arch == "x86_64":
        cross_compile_arch = "arm64"
    else:
        raise UnsupportedArchitecture(f"Architecture {arch} is not supported.")

    call(
        [
            REPO_ROOT_PATH / "Utilities" / "bootstrap",
            "build",
            "--release",
            "--verbose",
            "--cross-compile-hosts",
            f"macosx-{cross_compile_arch}",
            "--skip-cmake-bootstrap",
            "--swift-build-path",
            (swiftpm_bin_dir / "swift-build").resolve(),
        ],
    )


GlobalArgsValueType = str


@dataclasses.dataclass
class GlobalArgs:
    global_argument: str
    value: t.Optional[GlobalArgsValueType]


def filterIsTruthy(items: t.Iterable) -> t.Iterable:
    return list(filter(lambda x: x, items))

def log_directory_contents(directory: pathlib.Path) -> None:
    logging.info("Build path directory      : %s", directory)
    logging.info("Listing files in directory: %s", directory)
    try:
        for item in sorted(os.listdir(path=directory)):
            logging.info(" -> %s", item)
    except FileNotFoundError:
        logging.warning("Directory %s does not exist.", directory)


def main() -> None:
    args = get_arguments()
    logging.getLogger().setLevel(logging.DEBUG if args.is_verbose else logging.INFO)
    logging.debug("Args: %r", args)
    ignore_args = ["-Xlinker", "/ignore:4217"] if os.name == "nt" else []
    globalArgsData = [
        GlobalArgs(global_argument="--triple", value=args.triple),
        GlobalArgs(global_argument="--build-system", value=args.build_system),
        GlobalArgs(global_argument="--configuration", value=args.config),
    ]
    global_args: t.Iterator[GlobalArgsValueType] = list(
        itertools.chain.from_iterable(
            [[arg.global_argument, arg.value] for arg in globalArgsData if arg.value]
        )
    )
    logging.debug("Global Args: %r", global_args)
    start_time = datetime.now()
    with change_directory(REPO_ROOT_PATH) as dir:
        swiftpm_bin_dir = get_swiftpm_bin_dir(global_args=global_args)
        set_environment()

        call(
            filterIsTruthy(
                [
                    "swift",
                    "--version",
                ]
            )
        )

        if args.should_clean:
            call(
                filterIsTruthy(
                    [
                        "swift",
                        "package",
                        *global_args,
                        "clean",
                    ]
                )
            )

        if args.should_update:
            call(
                filterIsTruthy(
                    [
                        "swift",
                        "package",
                        *global_args,
                        "update",
                    ]
                )
            )

        call(
            filterIsTruthy(
                [
                    "swift",
                    "build",
                    *global_args,
                    "--build-tests",
                    *ignore_args,
                    *args.additional_build_args.split(" ")
                ]
            )
        )

        call(
            filterIsTruthy(
                [
                    "swift",
                    "run",
                    *global_args,
                    *ignore_args,
                    *args.additional_run_args.split(" "),
                    "swift-test",
                    *global_args,
                    "--vv",
                    "--force-resolved-versions",
                    "--parallel",
                    "--scratch-path",
                    ".test",
                    *ignore_args,
                    *args.additional_test_args.split(" ")
                ]
            )
        )

    if is_on_darwin() and not args.skip_bootstrap:
        run_bootstrap(swiftpm_bin_dir=swiftpm_bin_dir)

    end_time = datetime.now()
    elapsed_time = end_time - start_time

    logging.info("Done (%s)", str(elapsed_time))


if __name__ == "__main__":
    main()
